package com.zx.shiro.filter;

import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.ArrayDeque;
import java.util.Deque;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.DefaultSessionKey;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.apache.shiro.web.util.WebUtils;

/**
 * 自定义过滤器（用户访问控制）
 * 
 * 继承了Shiro的访问控制拦截器AccessControlFilter
 * AccessControlFilter还提供了如下方法用于处理如登录成功后/重定向到上一个请求：
 * void setLoginUrl(String loginUrl) //身份验证时使用，默认/login.jsp 
 * String getLoginUrl()  
 * Subject getSubject(ServletRequest request, ServletResponse response) //获取Subject实例  
 * boolean isLoginRequest(ServletRequest request, ServletResponse response)//当前请求是否是登录请求
 * void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException //将当前请求保存起来并重定向到登录页面 
 * void saveRequest(ServletRequest request) //将请求保存起来，如登录成功后再重定向回该请求  
 * void redirectToLogin(ServletRequest request, ServletResponse response) //重定向到登录页面
 * 
 * @author 郑雪
 * @date 2021-11-24
 */
public class KickoutSessionFilter extends AccessControlFilter {
	
	/**
	 * 踢出后到的地址
	 */
	private String kickoutUrl;
	
	/**
	 * 踢出之前登录的/之后登录的用户 默认false踢出之前登录的用户
	 */
	private boolean kickoutAfter = false;
	
	/**
	 * 同一个帐号最大会话数 默认1
	 */
	private int maxSession = 1;
	
	/**
	 * Shiro的session管理器
	 */
	private SessionManager sessionManager;
	
	/**
	 * ehcache登录缓存名称（必须和ehcache缓存名称一致）
	 * 如果需要缓存的话，指定一个缓存的名字
	 */
	private String loginSessionCacheName;
	
	/**
	 * 缓存（已登录的用户session缓存，从ehcache中获取）
	 */
	private Cache<String, Deque<Serializable>> cache;
	
	/**
	 * 设置Cache的key的前缀
	 * @param cacheManager
	 */
	public void setCacheManager(CacheManager cacheManager) {
		//必须和ehcache缓存配置中的缓存name一致
		this.cache = cacheManager.getCache(loginSessionCacheName);
	}

	/**
	 * 是否允许访问 
	 * mappedValue就是[urls]配置中拦截器参数部分 
	 * 如果允许访问返回true，否则false；
	 */
	@Override
	protected boolean isAccessAllowed(ServletRequest request,
			ServletResponse response, Object mappedValue) throws Exception {
		return false;
	}

	/**
	 * 当访问拒绝时是否已经处理了 
	 * 如果返回true表示需要继续处理；如果返回false表示该拦截器实例已经处理了，将直接返回即可。
	 */
	@Override
	protected boolean onAccessDenied(ServletRequest request, ServletResponse response)
			throws Exception {
		// 思路：
		// a. 登陆成功时将用户保存到了shiro提供的session中,并同时添加到ehcache缓存中；
		// b. KickoutSessionFilter拿到了session之后先判断能不能通过缓存取到值，如果取得到再和服务器端session进行匹配（用户的名字（每个用户的名字必须不同））；
		// c. 如果匹配,系统会为新登录的用户新建一个session;之前的session确认失效并踢出,老用户就无法继续操作而被迫下线；
		System.out.println("[ KickoutSessionFilter onAccessDenied ]");
		
		// 获取当前用户
		Subject subject = getSubject(request, response);
		// 没有登录授权 且没有记住我
		if (!subject.isAuthenticated() && !subject.isRemembered()) {
			// 如果没有登录，直接进行之后的流程，因为缓存中只为了存登录成功后的用户，即活跃用户
			return true;
		}
		// 获取当前用户的会话
		Session session = subject.getSession();
		System.out.println(
				"当前用户会话开始时间： "
				+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(session.getStartTimestamp())
				+ " 最后更新时间：　"
				+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(session.getLastAccessTime())
				);
		
		// 当前用户名
		String username = (String)subject.getPrincipal();
		System.out.println("当前用户名：" + username);
		Serializable sessionId = session.getId();
		System.out.println("当前sessionId：" + sessionId);
		// 读取缓存用户 没有就存入
		Deque<Serializable> deque = cache.get(username);
		System.out.println("当前缓存中该用户的队列：" + deque);	
		if(deque == null){
			// 初始化队列
			deque = new ArrayDeque<Serializable>();
		}
		// 如果队列里没有此sessionId，且用户没有被踢出；放入队列
		if (!deque.contains(sessionId)
				&& session.getAttribute("kickout") == null) {
			// 将sessionId存入队列
			deque.push(sessionId);
			// 将用户的sessionId队列缓存
			cache.put(username, deque);
		}
		// 如果队列里的sessionId数超出最大会话数，开始踢人
		while (deque.size() > maxSession) {
			System.out.println("当前缓存中该用户的队列长度：" + deque.size());	
			Serializable kickoutSessionId = null;
			// 是否踢出后来登录的，默认是false；即后者登录的用户踢出前者登录的用户；
			if (kickoutAfter) { // 如果踢出后者
				kickoutSessionId = deque.removeFirst();
			} else { // 否则踢出前者
				kickoutSessionId = deque.removeLast();
			}
			// 踢出后再更新下缓存队列
			cache.put(username, deque);
			// 获取被踢出的sessionId的session对象
			Session kickoutSession = sessionManager
					.getSession(new DefaultSessionKey(kickoutSessionId));
			if (kickoutSession != null) {
				// 设置会话的kickout属性表示踢出了
				kickoutSession.setAttribute("kickout", true);
				//如果想直接让对方的客户端浏览器跳转到登录页面，需要使用WebSocket给客户端发送消息，客户端收到该消息后跳转到登录页
			}
		}
		System.out.println("当前缓存中该用户的队列：" + deque);	
		// 如果被踢出了，(前者或后者)直接退出，重定向到踢出后的地址
		if ((Boolean) session.getAttribute("kickout") != null
				&& (Boolean) session.getAttribute("kickout") == true) {
			// 会话被踢出了
			// 退出登录
			subject.logout();
			//如果配置了踢出后跳转路径
			if(kickoutUrl != null){
				saveRequest(request);
				System.out.println("==踢出后用户重定向的路径kickoutUrl:" + kickoutUrl);
				// 重定向
				WebUtils.issueRedirect(request, response, kickoutUrl);
			}
			//如果没有配置踢出后的跳转路径，默认跳转配置中的登录页面
			else{
				System.out.println("==踢出后用户默认重定向到登录:" + getLoginUrl());
				saveRequestAndRedirectToLogin(request, response);
			}
			return false;
		}
		return true;
	}

	public void setKickoutUrl(String kickoutUrl) {
		this.kickoutUrl = kickoutUrl;
	}

	public void setKickoutAfter(boolean kickoutAfter) {
		this.kickoutAfter = kickoutAfter;
	}

	public void setMaxSession(int maxSession) {
		this.maxSession = maxSession;
	}

	public void setSessionManager(SessionManager sessionManager) {
		this.sessionManager = sessionManager;
	}

	public void setLoginSessionCacheName(String loginSessionCacheName) {
		this.loginSessionCacheName = loginSessionCacheName;
	}
	
}
